flutter - 数据流
前言
前不久的谷歌 io 上有几场关于 flutter 的演讲,其中 Build reactive mobile apps with Flutter 的一场主题演讲令我印象深刻。它简单易懂的讲述了一个 flutter app 该如何实现界面与数据和状态的绑定,这里希望可以通过这篇文章记录一下学习成果。我也在 github 上找到了该演讲演示的项目代码 state_experiments,当然你如果看到这篇文章,我希望你能有一些 flutter 的基础
setState() 方法
flutter 是一种响应式的界面编程,数据与视图分离,原理与 react 比较相似。在外部会构建出 Widget Tree(开销很小,每次都会重建),进而会在内部构建出 Render Tree(其实中间还有一个 Element Tree,这个新建销毁的开销相对较大,所以会通过一些 diff 算法来避免不必要的操作)。日常开发主要在于 Widget Tree 的构建工作,而 flutter 的 widget 主要分为StatelessWidget 和 StatefulWidget 两种,前者是一种无状态的控件,用于展示静态内容,而后者则是动态的,数据通过状态的形式与界面发生交互逻辑。而该空间的数据有且只有通过 setState 方式才能触发界面的更新(为 Element 节点设置 dirty 标志位,调用 build 方法)。
如果在 Widget Tree 的两个分支都需要用到同一个 state 时麻烦的点就来,我们需要将 state 定义在两个分支的根节点,然后需要将 state 沿着树的分支往下传递到目标节点。当层次较深时,这种方式显然是不能接受的。如图所示
InheritedWidget
该 widget 能将数据沿着 widget tree 高效地向下传递,通过定义静态方法 InheritedWidget.of(context) 方法可以获取到唯一实例,其实是通过上下文 context. inheritFromWidgetOfExactType 方法来取。
以一个简单的定义为例
1 | class FrogColor extends InheritedWidget { |
通过定义的静态方法 of,我们可以通过 BuildContext.inheritFromWidgetOfExactType 方法在同一个上下文环境(同一颗 widget tree)中得到同一个实例。另外我们也可以在静态方法中实现我们自己的逻辑,比如说直接获取到 color 或者做一个空判断的逻辑等等
在 flutter 中,视图的更新需要调用 StatefulWidget 的 setState 方法,那么视图应该如何响应通过 InheritedWidget 传递的数据的变化呢,下面有几种方案可以参考实施
ScopedModel
在 pubspec.yaml 中引入 scoped_model 包
- 定义自己的数据 model 继承自 Model 类,在数据改变时调用 notifyListeners() 方法
1 | class MyModel extend Model{ |
- 在 widget tree 中插入一个 ScopedModel
的 widget,将数据类实例赋值给 model 变量
1 | Widget build(BuildContext context) { |
- 在 widget tree 需要调用数据的地方使用 ScopedModelDescendant
,通过 Widget ScopedModelDescendantBuilder (BuildContext context, Widget child, T model) 方法构造 child widget
1 | ScopedModelDescendant<CartModel>( |
- 对于事件源,同样需要获取到 model 实例,对于事件源不需要刷新界面的场景,需要对 rebuildOnChange 参数设置,当该参数为 false 时,界面不会响应数据的变化
1 | ScopedModelDescendant<CartModel>( |
原理
ScopedModel 继承自 StatelessWidget,但他的 build 方法构造了两个关键的 widget
1 | class ScopedModel<T extends Model> extends StatelessWidget { |
_ModelListener 是一个继承自 StatefulWidget 的控件,model 变化通过 notifyListeners 方法传递到该控件中触发 setState 操作,从而调用 builder 响应变化。而 _InheritedModel 则继承自 InheritedWidget 用于传递数据。
而 ScopedModelDescendant 内部会通过 ModelFinder 类去通过上下文去获取 _InheritedModel 传递的实例将数据绑定到界面上,当 rebuildOnChange = false 时,会通过 _InheritedModel.ancestorWidgetOfExactType(type) 获取数据,界面不会响应数据的变化
Bloc & Stream(rxdart版)
Bloc 指 Business logic,用于承载逻辑,通过 stream 响应式编程实现。Android 开发小伙伴肯定熟悉 RxJava,在 flutter 中我们同样可以通过 rxdart 来实现响应式数据流
- 定义数据类,然后定义 Bloc 类,创建 StreamController
作为被观察者,创建 Subject 作为订阅者,通过 StreamController.stream.listen 产生订阅关系
1 | class MyBloc { |
- 定义 Provider 继承 InheritedWidget 作为获取 Bloc 实例的路径
1 | class MyBlocProvider extends InheritedWidget { |
- 事件源可以直接通过 Provider.of(context) 获取到的实例对 StreamController.sink 添加事件
1 | Widget build(BuildContext context) { |
- 界面响应则需要 StreamBuilder 进行包裹,需要添加 stream 流,为 Bloc 中 Subject.stream,通过 Widget AsyncWidgetBuilder
(BuildContext context, AsyncSnapshot snapshot) 方法是界面响应数据变化
1 | Widget build(BuildContext context) { |
原理
StreamBuilder 继承自 StreamBuilderBase, 而
它又继承自 StatefulWidget,在 _StreamBuilderBaseState 中有对应的更新逻辑
1 | class _StreamBuilderBaseState<T, S> extends State<StreamBuilderBase<T, S>> { |
在 state 中维护了一个订阅关系 StreamSubscription,传入的 stream 流会在 initState 以及 didUpdateWidget 方法中通过 _subscribe 方法被订阅。当 stream 中传递了新的数据时就会触发 widget 的 setState 方法去更新 _summary 的值即我们的 AsyncSnapshot 使用的值从而触发界面的更新
与 ScopedModel 比较
由于 ScopedModel 是在最顶层的 ScopedModel 中去响应数据的改变,虽然 flutter 内部有相应的 diff 操作来避免额外的界面刷新,但还是有些过重,同样由于内部集成了_InheritedModel 导致需要 rebuildOnChange 去阻止一些类似事件源的非必要刷新。
而 StreamBuilder 则只是在对应的节点进行刷新操作,而且对于响应式编程中数据的操作更加的灵活,代码也更加的简洁
Redux
想必开发过 rn 项目的小伙伴对于 redux 这种架构并不陌生吧,作为一个响应式界面开发框架,flutter 也有第三方的 flutter_redux 实现
在 pubspec.yaml 中引入 flutter_redux 包
- 定义数据类,然后定义好 Action 以及对应的 Reducer
1 | class InputAction { |
- 创建 Store
类实例,传入 Reducer 和初始化的 state 状态,在 widget tree 中插入 StoreProvider\ ,并将创建好的 store 实例传入
1 | Data myReducer(Data state, dynamic action) { |
1 | final store = Store<Data>(myReducer, initialState: Data()); |
- 在 widget tree 中通过 StoreConnector\<Store, ViewModel> 来构造界面,通过 converter 函数实现 Store 到 ViewModel 的转换(可以是获取 state 数据,也可以是获取 dispatch 的 callback 函数),再通过 builder 函数生成界面
界面
1 | StoreConnector<Data, Output>( |
事件源
1 | StoreConnector<Data, Function(Input)>( |
原理
StoreProvider 继承自 InheritedWidget,用于存储 store 实例。而 StoreConnector 继承自 StatelessWidget 但是会返回 _StoreStreamListener 的继承自 StatefulWidget 的 widget,内部则是通过 StreamBuilder 去实现响应流的,获取的 stream 来自于 store.onChange 即内部变量 _changeController.stream
store.dispatch 方法有如下处理
1 | NextDispatcher _createReduceAndNotify(bool distinct) { |
每次分发 action 时就会触发 stream 流
结语
对于一般的页面而言,InheritedWidget 构造出 DataProvider 足矣,但对于复杂页面的交互就需要 redux,bloc+stream 帮忙了。个人而言更倾向于 blco+stream 这种方式更加的灵活,加之日后生态成熟了以后一些扩展功能对于 rxdart 之类的响应式框架更加友好。而基于 stream 编写的 redux 则对于一些前端开发者更加友好,但确实扩展性稍显不足。
希望有想更深入了解可以去看一下前言t
自己的 io 演讲,以及对应的 github 项目。